基础 VS 导入第三方 lib 库 待更新
获取进程句柄 这里指的是如何通过进程名来获取目标进程 id 。这样做的好处是代码更具有通用性,因为通常每次程序启动 pid 会变化但进程名不变。
CreateToolhelp32Snapshot
函数用于创建一个包含系统中进程、线程、模块或堆的快照。我们使用它来获取当前系统中所有进程的快照。
1 2 3 4 cppCopy codeHANDLE CreateToolhelp32Snapshot ( DWORD dwFlags, DWORD th32ProcessID ) ;
dwFlags :指定快照的内容。对于进程快照,使用 TH32CS_SNAPPROCESS
。常用值含义如下:
TH32CS_SNAPPROCESS
:表示进程快照。
TH32CS_SNAPTHREAD
:表示线程快照。
TH32CS_SNAPMODULE
:表示模块快照。
TH32CS_SNAPHEAPLIST
:表示堆快照。
th32ProcessID :指定要快照的进程的 ID 。0 表示获取所有进程的快照。
返回值 :如果成功,返回快照的句柄;如果失败,返回 INVALID_HANDLE_VALUE
。
Process32First
函数用于获取进程快照中的第一个进程的信息。
1 2 3 4 cppCopy codeBOOL Process32First ( HANDLE hSnapshot, LPPROCESSENTRY32 lppe ) ;
hSnapshot :由 CreateToolhelp32Snapshot
返回的快照句柄。
lppe :指向 PROCESSENTRY32
结构的指针,用于接收第一个进程的信息。
返回值 :如果成功,返回非零值;如果失败或没有更多进程,返回零。
Process32Next
函数用于获取快照中的下一个进程的信息。
1 2 3 4 cppCopy codeBOOL Process32Next ( HANDLE hSnapshot, LPPROCESSENTRY32 lppe ) ;
hSnapshot :由 CreateToolhelp32Snapshot
返回的快照句柄。
lppe :指向 PROCESSENTRY32
结构的指针,用于接收下一个进程的信息。
返回值 :如果成功,返回非零值;如果失败或没有更多进程,返回零。
利用上述 API 可以枚举系统中的所有进程,以找到目标进程的进程 ID(PID)。注意,同一个程序可能会创建多个进程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <windows.h> #include <tlhelp32.h> #include <tchar.h> #include <iostream> DWORD GetTargetProcessId (const TCHAR *processName) { DWORD processId = -1 ; HANDLE hSnapshot = CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0 ); if (hSnapshot != INVALID_HANDLE_VALUE) { PROCESSENTRY32 pe; pe.dwSize = sizeof (pe); if (Process32First (hSnapshot, &pe)) { do { if (_tcsicmp(pe.szExeFile, processName) == 0 ) { processId = pe.th32ProcessID; break ; } } while (Process32Next (hSnapshot, &pe)); } CloseHandle (hSnapshot); } return processId; } int _tmain() { const TCHAR *targetProcessName = _T("target.exe" ); std::cout << GetTargetProcessId (targetProcessName) << std::endl; }
注入技术 远程线程注入 远程线程注入是一种将代码注入到目标进程的方法。它通过在目标进程中创建一个新的线程来执行我们指定的代码。
步骤
获取目标进程的句柄。
在目标进程的地址空间中分配内存。
将要执行的代码(如DLL路径)写入目标进程的内存中。
使用 CreateRemoteThread
函数在目标进程中创建一个新线程,使其执行我们指定的代码(如 LoadLibrary
函数)。
API 介绍 OpenProcess
函数用于打开一个已存在的进程,并返回进程的句柄。
1 2 3 4 5 HANDLE OpenProcess ( DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId ) ;
dwDesiredAccess :指定所需的访问权限。常用值:
PROCESS_ALL_ACCESS
:表示所有可能的权限。
PROCESS_CREATE_THREAD
:允许创建线程。
PROCESS_VM_OPERATION
:允许虚拟内存操作。
PROCESS_VM_READ
:允许读取进程的内存。
PROCESS_VM_WRITE
:允许写入进程的内存。
bInheritHandle :指定新句柄是否可以被当前进程创建的子进程继承。如果为 TRUE ,则可以继承;否则不能。
dwProcessId :指定要打开的进程的 ID 。
返回值 :如果成功,返回进程的句柄;如果失败,返回 NULL
。
VirtualAllocEx
函数用于在目标进程的地址空间中分配内存。
1 2 3 4 5 6 7 cppCopy codeLPVOID VirtualAllocEx ( HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect ) ;
hProcess :目标进程的句柄。
lpAddress :指定内存地址,可以为 NULL
,表示由系统决定分配位置。
dwSize :要分配的内存大小。
flAllocationType :内存分配类型。常用值:
MEM_COMMIT
:分配物理内存并将其初始化为零。
MEM_RESERVE
:保留一块虚拟地址空间,但不分配物理内存。
flProtect :内存保护属性。常用值:
PAGE_READWRITE
:可读写的内存。
PAGE_READONLY
:只读内存。
PAGE_EXECUTE
:可执行内存。
返回值 :如果成功,返回分配的内存地址;如果失败,返回NULL
。
WriteProcessMemory
函数用于将数据写入目标进程的内存中。
1 2 3 4 5 6 7 BOOL WriteProcessMemory ( HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesWritten ) ;
hProcess :目标进程的句柄。
lpBaseAddress :目标进程的内存地址。
lpBuffer :要写入的数据缓冲区。
nSize :要写入的数据大小。
lpNumberOfBytesWritten :实际写入的数据大小,可以为 NULL
。
返回值 :如果成功,返回非零值;如果失败,返回零。
CreateRemoteThread
函数用于在目标进程中创建一个新线程。
1 2 3 4 5 6 7 8 9 HANDLE CreateRemoteThread ( HANDLE hProcess, LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId ) ;
hProcess :目标进程的句柄。
lpThreadAttributes :线程安全属性,可以为 NULL
。
dwStackSize :线程的堆栈大小,可以为 0 。
lpStartAddress :新线程的起始地址。
lpParameter :传递给新线程的参数。
dwCreationFlags :线程创建标志,常用的是 0 。
lpThreadId :返回新线程的 ID ,可以为 NULL
。
返回值 :如果成功,返回新线程的句柄;如果失败,返回 NULL
。
代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 #include <windows.h> #include <tlhelp32.h> #include <tchar.h> #include <iostream> DWORD GetTargetProcessId (const TCHAR* processName) { DWORD processId = 0 ; HANDLE hSnapshot = CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0 ); if (hSnapshot != INVALID_HANDLE_VALUE) { PROCESSENTRY32 pe; pe.dwSize = sizeof (pe); if (Process32First (hSnapshot, &pe)) { do { if (_tcsicmp(pe.szExeFile, processName) == 0 ) { processId = pe.th32ProcessID; break ; } } while (Process32Next (hSnapshot, &pe)); } CloseHandle (hSnapshot); } return processId; } BOOL InjectDLL (DWORD processId, const TCHAR *dllPath) { HANDLE hProcess = OpenProcess (PROCESS_ALL_ACCESS, FALSE, processId); if (hProcess == NULL ) { _tprintf(_T("[-] Failed to open target process.\n" )); return FALSE; } TCHAR fullDllPath[MAX_PATH]; if (!GetFullPathName (dllPath, MAX_PATH, fullDllPath, NULL )) { _tprintf(_T("[-] Failed to get full path of DLL.\n" )); return FALSE; } LPVOID pRemoteBuf = VirtualAllocEx (hProcess, NULL , (_tcslen(fullDllPath) + 1 ) * sizeof (TCHAR), MEM_COMMIT, PAGE_READWRITE); if (pRemoteBuf == NULL ) { CloseHandle (hProcess); _tprintf(_T("[-] Failed to allocate memory in target process.\n" )); return FALSE; } if (!WriteProcessMemory (hProcess, pRemoteBuf, (LPVOID) fullDllPath, (_tcslen(fullDllPath) + 1 ) * sizeof (TCHAR), NULL )) { VirtualFreeEx (hProcess, pRemoteBuf, 0 , MEM_RELEASE); CloseHandle (hProcess); _tprintf(_T("[-] Failed to write DLL path to target process memory.\n" )); return FALSE; } HMODULE hKernel32 = GetModuleHandle (_T("Kernel32.dll" )); LPVOID pLoadLibrary = NULL ; #ifdef UNICODE pLoadLibrary = (LPVOID) GetProcAddress (hKernel32, "LoadLibraryW" ); #else pLoadLibrary = (LPVOID) GetProcAddress (hKernel32, "LoadLibraryA" ); #endif if (pLoadLibrary == NULL ) { VirtualFreeEx (hProcess, pRemoteBuf, 0 , MEM_RELEASE); CloseHandle (hProcess); _tprintf(_T("[-] Failed to get address of LoadLibrary.\n" )); return FALSE; } HANDLE hThread = CreateRemoteThread (hProcess, NULL , 0 , (LPTHREAD_START_ROUTINE) pLoadLibrary, pRemoteBuf, 0 , NULL ); if (hThread == NULL ) { VirtualFreeEx (hProcess, pRemoteBuf, 0 , MEM_RELEASE); CloseHandle (hProcess); _tprintf(_T("[-] Failed to create remote thread in target process.\n" )); return FALSE; } WaitForSingleObject (hThread, INFINITE); VirtualFreeEx (hProcess, pRemoteBuf, 0 , MEM_RELEASE); CloseHandle (hThread); CloseHandle (hProcess); _tprintf(_T("[+] DLL injected successfully.\n" )); return TRUE; } int _tmain() { const TCHAR* targetProcessName = _T("target_process.exe" ); const TCHAR* dllPath = _T("inject_dll.dll" ); DWORD processId = GetTargetProcessId (targetProcessName); if (processId == 0 ) { _tprintf(_T("[-] Target process not found.\n" )); return 1 ; } if (InjectDLL (processId, dllPath)) { _tprintf(_T("[+] DLL injected successfully.\n" )); } else { _tprintf(_T("[-] Failed to inject DLL.\n" )); } return 0 ; }
Hook 技术 Inline Hook Inline Hook 的演变 首先最原始的 Inline Hook 是直接暴力的在 targetFunc
的开头 patch 一条跳转到 hookFunc
的汇编指令。这样程序在调用函数的时候会调用到我们的自己实现的 Hook 函数上。 然而这样存在 Hook 重入问题,即如果我们想在 hookFunc
中调用 targetFunc
会出现无限递归的情况,因此在 hookFunc
调用 targetFunc
前需要恢复 targetFunc
前面被 patch 的部分。而调用 targetFunc
后需要重新恢复 Hook 。 然而这种方法每次调用 targetFunc
前后都要修改 targetFunc
开头,因此如果多个线程同时调用 targetFunc
会出现多线程竞争问题。 因此可以借助跳板函数 originalFunc
来解决重入问题,这样就不需要多次修改 targetFunc
,也就没有线程竞争问题。 在 originalFunc
中我们会执行 targetFunc
中被 Hook 点覆盖的指令,从而确保 targetFunc
的完整性,然而 targetFunc
前面对应跳转指令长度的指令不一定是完整的指令。然而由于 Hook 是 Windows 中常用的手段,因此微软在开发 Windows API 时会确保函数开头处的指令长度对应跳转指令长度。
例如在 32 位下,LoadLibraryW
函数开头通过添加 mov edi, edi
指令恰好凑出了总长度为 5 字节的指令,这也正是 32 位下跳转指令的长度。
1 2 3 4 KERNEL32!LoadLibraryWStub: 76f3d8a0 8bff mov edi ,edi 76f3d8a2 55 push ebp 76f3d8a3 8bec mov ebp ,esp
在 64 位下,LoadLibraryW
函数开头是一个跳转指令并且后面用 INT3
指令填充,因此我们可以使用 FF 25
的跳转指令来实现任意地址跳转。
1 2 3 4 5 6 7 8 9 KERNEL32!LoadLibraryWStub: 00007ff8`db4b8be0 48ff2539be0600 jmp qword ptr [KERNEL32!_imp_LoadLibraryW (00007ff8` db524a20)] 00007ff8`db4b8be7 cc int 3 00007ff8` db4b8be8 cc int 3 00007ff8`db4b8be9 cc int 3 00007ff8` db4b8bea cc int 3 00007ff8`db4b8beb cc int 3 00007ff8` db4b8bec cc int 3 00007ff8`db4b8bed cc int 3
FF 25 00 00 00 00
的跳转指令翻译过来是 jmp qword prt ds:[下一条指令地址]
,因此只要紧跟着 8 字节的要跳转到的地址就可以实现任意地址跳转,总指令长度为 14 字节。
这里简单实现了一下对 MessageBox
函数的 Hook 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 #include "pch.h" #include <iostream> #include <tchar.h> #include <windows.h> typedef int (WINAPI *MessageBox_t) (HWND, LPCTSTR, LPCTSTR, UINT) ;MessageBox_t OriginalMessageBox = nullptr ; #ifdef _WIN64 constexpr size_t byteCount = 14 ;#else constexpr size_t byteCount = 5 ;#endif int WINAPI HookedMessageBox (HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType) { _tprintf(_T("[+] Hooked MessageBox called with text: %s\n" ), lpText); return OriginalMessageBox (hWnd, _T("Hooked!" ), lpCaption, uType); } void InlineHook (void *targetFunc, void *hookFunc, void **originalFunc) { DWORD oldProtect; *originalFunc = VirtualAlloc (NULL , byteCount * 2 , MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (*originalFunc == NULL ) { _tprintf(_T("[-] Failed to allocate memory for trampoline.\n" )); return ; } VirtualProtect (targetFunc, byteCount, PAGE_EXECUTE_READWRITE, &oldProtect); memcpy (*originalFunc, targetFunc, byteCount); #ifdef _WIN64 *(BYTE *) ((DWORD64) *originalFunc + byteCount) = (BYTE) 0xFF ; *(BYTE *) ((DWORD64) *originalFunc + byteCount + 1 ) = 0x25 ; *(DWORD *) ((DWORD64) *originalFunc + byteCount + 2 ) = 0x00000000 ; *(DWORD64 *) ((DWORD64) *originalFunc + byteCount + 6 ) = (DWORD64) targetFunc + byteCount; *(BYTE *) targetFunc = (BYTE)0xFF ; *(BYTE *) ((DWORD64) targetFunc + 1 ) = 0x25 ; *(DWORD *) ((DWORD64) targetFunc + 2 ) = 0x00000000 ; *(DWORD64 *) ((DWORD64) targetFunc + 6 ) = (DWORD64) hookFunc; #else DWORD targetAddr = (DWORD) targetFunc + byteCount; DWORD trampolineAddr = (DWORD) *originalFunc + byteCount; *(BYTE *) ((DWORD) *originalFunc + byteCount) = 0xE9 ; *(DWORD *) ((DWORD) *originalFunc + byteCount + 1 ) = targetAddr - (trampolineAddr + byteCount); *(BYTE *) targetFunc = 0xE9 ; *(DWORD *) ((DWORD) targetFunc + 1 ) = (DWORD) hookFunc - ((DWORD) targetFunc + byteCount); #endif VirtualProtect (targetFunc, byteCount, oldProtect, &oldProtect); _tprintf(_T("[+] Inline hook installed successfully.\n" )); } void Unhook (void *targetFunc, void *originalFunc) { DWORD oldProtect; VirtualProtect (targetFunc, byteCount, PAGE_EXECUTE_READWRITE, &oldProtect); memcpy (targetFunc, originalFunc, byteCount); VirtualProtect (targetFunc, byteCount, oldProtect, &oldProtect); VirtualFree (originalFunc, 0 , MEM_RELEASE); _tprintf(_T("[+] Inline hook removed successfully.\n" )); } void InstallHook () { InlineHook ((void *) MessageBoxW, (void *) HookedMessageBox, (void **) &OriginalMessageBox); } void UninstallHook () { if (OriginalMessageBox) { Unhook ((void *) MessageBoxW, (void *) OriginalMessageBox); } } BOOL APIENTRY DllMain (HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: _tprintf(_T("[+] DLL loaded.\n" )); InstallHook (); break ; case DLL_PROCESS_DETACH: _tprintf(_T("[+] DLL unloaded.\n" )); UninstallHook (); break ; } return TRUE; }
然而在 64 位运行时会发现出错了,这是因为 64 位下的 MessageBox
的开头是如下代码,这里第三条指令如果只是复制到 originalFunc
没有重定位会导致访存错误。因此这种方法也是不完善的(需要假定微软预留了 Hook 的位置)。
1 2 3 4 USER32!MessageBoxW: 00007ff8`dae2b240 4883ec38 sub rsp,38h 00007ff8` dae2b244 4533db xor r11d ,r11d 00007ff8`dae2b247 44391d4af00300 cmp dword ptr [USER32!gfEMIEnable (00007ff8` dae6a298)],r11d
Detours 库 Detours 是由微软开发的一种用于拦截函数调用和重定向代码执行的动态链接库。它被广泛用于性能监控、调试、日志记录和安全检查等场景。Detours通过修改目标函数的入口点,实现对函数调用的拦截,并将调用重定向到用户定义的钩子函数。
Detours 库通过如下 API 完成 inline Hook 操作:
1 LONG DetourTransactionBegin () ;
DetourTransactionBegin
函数用于开始一个 Detours 事务。在事务中进行的所有操作都是原子的,也就是说,所有操作要么全部成功,要么全部失败,这样可以保证钩子操作的完整性和一致性。
初始化一个全局变量来保存事务的状态。
事务开始后,可以进行多个钩子操作(附加、分离等),直到事务提交或中止。
1 LONG DetourUpdateThread (HANDLE hThread) ;
DetourUpdateThread
函数将当前线程包含在 Detours 事务中,确保事务中的所有操作在同一个线程上下文中进行,防止多线程竞争问题。
将当前线程的上下文信息保存到事务中。
确保所有的钩子操作在同一个线程内执行,以避免多线程环境下的竞争和不一致。
1 PVOID DetourFindFunction (LPCSTR lpModule, LPCSTR lpFunction) ;
DetourFindFunction
函数用于查找指定模块中的函数地址。它帮助我们找到需要钩子的目标函数地址。
1 LONG DetourAttach (PVOID *ppPointer, PVOID pDetour) ;
DetourAttach
函数用于附加一个钩子函数,将目标函数重定向到钩子函数。
ppPointer
:指向目标函数指针的指针。
pDetour
:指向钩子函数的指针。
1 LONG DetourDetach (PVOID *ppPointer, PVOID pDetour) ;
DetourDetach
函数用于分离一个钩子函数,恢复目标函数的原始行为。
ppPointer
:指向目标函数指针的指针。
pDetour
:指向钩子函数的指针。
1 LONG DetourTransactionCommit () ;
DetourTransactionCommit
函数用于中止一个 Detours 事务,放弃所有未提交的钩子操作。
1 LONG DetourTransactionAbort () ;
DetourTransactionAbort
函数用于中止一个Detours事务,放弃所有未提交的钩子操作。
这里利用 Detours 库同样实现了对 MessageBox
的 Hook 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 #include "pch.h" #include <windows.h> #include <tchar.h> #include <iostream> #include "detours.h" #pragma comment(lib, "detours.lib" ) typedef int (WINAPI *MessageBox_t) (HWND, LPCTSTR, LPCTSTR, UINT) ;MessageBox_t OriginalMessageBox = nullptr; int WINAPI HookedMessageBox (HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType) { _tprintf(_T("[+] Hooked MessageBox called with text: %s\n" ), lpText); return OriginalMessageBox(hWnd, _T("Hooked!" ), lpCaption, uType); } void InstallHook () { LONG error = DetourTransactionBegin(); if (error != NO_ERROR) { _tprintf(_T("[-] DetourTransactionBegin failed: %d\n" ), error); return ; } error = DetourUpdateThread(GetCurrentThread()); if (error != NO_ERROR) { _tprintf(_T("[-] DetourUpdateThread failed: %d\n" ), error); DetourTransactionAbort(); return ; } OriginalMessageBox = (MessageBox_t) DetourFindFunction("user32.dll" , "MessageBoxW" ); if (OriginalMessageBox == NULL ) { _tprintf(_T("[-] DetourFindFunction failed.\n" )); DetourTransactionAbort(); return ; } error = DetourAttach(&(PVOID &) OriginalMessageBox, HookedMessageBox); if (error != NO_ERROR) { _tprintf(_T("[-] DetourAttach failed: %d\n" ), error); DetourTransactionAbort(); return ; } error = DetourTransactionCommit(); if (error != NO_ERROR) { _tprintf(_T("[-] DetourTransactionCommit failed: %d\n" ), error); return ; } _tprintf(_T("[+] Hook installed successfully.\n" )); } void UninstallHook () { LONG error = DetourTransactionBegin(); if (error != NO_ERROR) { _tprintf(_T("[-] DetourTransactionBegin failed: %d\n" ), error); return ; } error = DetourUpdateThread(GetCurrentThread()); if (error != NO_ERROR) { _tprintf(_T("[-] DetourUpdateThread failed: %d\n" ), error); DetourTransactionAbort(); return ; } error = DetourDetach(&(PVOID &) OriginalMessageBox, HookedMessageBox); if (error != NO_ERROR) { _tprintf(_T("[-] DetourDetach failed: %d\n" ), error); DetourTransactionAbort(); return ; } error = DetourTransactionCommit(); if (error != NO_ERROR) { _tprintf(_T("[-] DetourTransactionCommit failed: %d\n" ), error); return ; } _tprintf(_T("[+] Hook removed successfully.\n" )); } BOOL APIENTRY DllMain (HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: DetourRestoreAfterWith(); InstallHook(); break ; case DLL_PROCESS_DETACH: UninstallHook(); break ; } return TRUE; }
以 64 位 Hook 为例,观察发现 Detour 库的实现基本原理和我们自己实现的 Inline Hook 基本相同,不过 Detour 库实现的 Hook 点长度更短。另外由于 Detour 是微软官方使用的,兼容性更强,同时还加入了大量的锁操作确保线程安全,因此推荐使用 Detour 来实现 Inline Hook 。